<?php
/*********************/
/*                   */
/* http://www.ymd.cc */
/*********************/
/*    QQ: 11121426   */
/*                   */
/*********************/

if (!defined("SECACHE_SIZE")) {
    define("SECACHE_SIZE", "15M");
}

class secache extends cachemgr
{

    public $idx_node_size = 40;
    public $data_base_pos = 262588;
    public $schema_item_size = 24;
    public $header_padding = 20;
    public $info_size = 20;
    public $idx_seq_pos = 40;
    public $dfile_cur_pos = 44;
    public $idx_free_pos = 48;
    public $idx_base_pos = 444;
    public $min_size = 10240;
    public $schema_struct = array
    (
        0 => "size",
        1 => "free",
        2 => "lru_head",
        3 => "lru_tail",
        4 => "hits",
        5 => "miss"
    );
    public $ver = "\$Rev: 38548 \$";
    public $name = "secache";

    public function secache()
    {
        $this->workat(HOME_DIR . "/cache/cachedata");
        $statfile = HOME_DIR . "/cache/cachedata.stat.php";
        if (file_exists($statfile)) {
            $this->_stat_rs = fopen($statfile, "rb+");
            $contents = "";
            while (!feof($this->_stat_rs)) {
                $contents .= fread($this->_stat_rs, 4096);
            }
            $this->_vary_list = unserialize($contents);
        } else {
            $this->_stat_rs = fopen($statfile, "wb+");
            $this->_vary_list = array();
        }
    }

    public function setModified($key)
    {
        $now = time();
        if (is_array($key)) {
            foreach ($key as $k) {
                $this->_vary_list[strtoupper($k)] = $now;
            }
        } else {
            $this->_vary_list[strtoupper($key)] = $now;
        }
        fseek($this->_stat_rs, 0);
        ftruncate($this->_stat_rs, 0);
        return fputs($this->_stat_rs, serialize($this->_vary_list));
    }

    public function workat($file)
    {
        $this->_file = $file . ".php";
        $this->_bsize_list = array(512 => 10, 3072 => 10, 8192 => 10, 20480 => 4, 30720 => 2, 51200 => 2, 81920 => 2, 98304 => 2, 131072 => 2, 229376 => 2, 262144 => 2, 524288 => 1, 1048576 => 1);
        $this->_node_struct = array(
            "next" => array(0, "V"),
            "prev" => array(4, "V"),
            "data" => array(8, "V"),
            "size" => array(12, "V"),
            "lru_right" => array(16, "V"),
            "lru_left" => array(20, "V"),
            "key" => array(24, "H*")
        );
        if (!file_exists($this->_file)) {
            $this->create();
        } else {
            if (!($this->_rs = fopen($this->_file, "rb+"))) {
                $this->trigger_error("Can't open the cachefile: " . realpath($this->_file), E_USER_ERROR);
            }
            $this->_seek($this->header_padding);
            $info = unpack("V1max_size/a*ver", fread($this->_rs, $this->info_size));
            if ($info['ver'] != $this->ver) {
                $this->_format(true);
            } else {
                $this->max_size = $info['max_size'];
            }
        }
        $this->idx_node_base = $this->data_base_pos + $this->max_size;
        $this->_block_size_list = array_keys($this->_bsize_list);
        sort($this->_block_size_list);
        return true;
    }

    public function create()
    {
        if (!($this->_rs = fopen($this->_file, "wb+"))) {
            $this->trigger_error("Can't open the cachefile: " . realpath($this->_file), E_USER_ERROR);
        }
        if (!is_writable($this->_file) || !is_readable($this->_file)) {
            chmod($this->_file, 438);
        }
        fseek($this->_rs, 0);
        fputs($this->_rs, "<?php exit()?>");
        return $this->_format();
    }

    public function _puts($offset, $data)
    {
        if ($offset < $this->max_size * 1.5) {
            $this->_seek($offset);
            return fputs($this->_rs, $data);
        } else {
            $this->trigger_error("Offset over quota:" . $offset, E_USER_ERROR);
        }
    }

    public function _seek($offset)
    {
        return fseek($this->_rs, $offset);
    }

    public function clear()
    {
        return $this->_format(true);
    }

    public function fetch($key, &$return)
    {
        if ($this->lock(false)) {
            $locked = true;
        }
        if ($this->search($key, $offset)) {
            $info = $this->_get_node($offset);
            $schema_id = $this->_get_size_schema_id($info['size']);
            if ($schema_id === false) {
                if ($locked) {
                    $this->unlock();
                }
                return false;
            }
            $this->_seek($info['data']);
            $data = fread($this->_rs, $info['size']);
            $return = unserialize($data);
            if ($return === false) {
                if ($locked) {
                    $this->unlock();
                }
                return false;
            }
            if ($locked) {
                $this->_lru_push($schema_id, $info['offset']);
                $this->_set_schema($schema_id, "hits", $this->_get_schema($schema_id, "hits") + 1);
                return $this->unlock();
            } else {
                return true;
            }
        } else {
            if ($locked) {
                $this->unlock();
            }
            return false;
        }
    }

    public function lock($is_block, $whatever = false)
    {
        ignore_user_abort(true);
        return flock($this->_rs, $is_block ? LOCK_EX : LOCK_EX + LOCK_NB);
    }

    public function unlock()
    {
        ignore_user_abort(false);
        return flock($this->_rs, LOCK_UN);
    }

    public function delete($key, $pos = false)
    {
        if (($pos || $this->search($key, $pos)) && ($info = $this->_get_node($pos))) {
            if ($info['prev']) {
                $this->_set_node($info['prev'], "next", $info['next']);
                $this->_set_node($info['next'], "prev", $info['prev']);
            } else {
                $this->_set_node($info['next'], "prev", 0);
                $this->_set_node_root($key, $info['next']);
            }
            $this->_free_dspace($info['size'], $info['data']);
            $this->_lru_delete($info);
            $this->_free_node($pos);
            return $info['prev'];
        }
        return false;
    }

    public function store($key, $value)
    {
        if ($this->lock(true)) {
            $data = serialize($value);
            $size = strlen($data);
            $has_key = $this->search($key, $list_idx_offset);
            $schema_id = $this->_get_size_schema_id($size);
            if ($schema_id === false) {
                $this->unlock();
                return false;
            }
            if ($has_key) {
                $hdseq = $list_idx_offset;
                $info = $this->_get_node($hdseq);
                if ($schema_id == $this->_get_size_schema_id($info['size'])) {
                    $dataoffset = $info['data'];
                } else {
                    $this->_lru_delete($info);
                    if (!($dataoffset = $this->_dalloc($schema_id))) {
                        $this->unlock();
                        return false;
                    }
                    $this->_free_dspace($info['size'], $info['data']);
                    $this->_set_node($hdseq, "lru_left", 0);
                    $this->_set_node($hdseq, "lru_right", 0);
                }
                $this->_set_node($hdseq, "size", $size);
                $this->_set_node($hdseq, "data", $dataoffset);
            } else {
                if (!($dataoffset = $this->_dalloc($schema_id))) {
                    $this->unlock();
                    return false;
                }
                $hdseq = $this->_alloc_idx(array(
                    "next" => 0,
                    "prev" => $list_idx_offset,
                    "data" => $dataoffset,
                    "size" => $size,
                    "lru_right" => 0,
                    "lru_left" => 0,
                    "key" => $key
                ));
                if (0 < $list_idx_offset) {
                    $this->_set_node($list_idx_offset, "next", $hdseq);
                } else {
                    $this->_set_node_root($key, $hdseq);
                }
            }
            if ($this->max_size < $dataoffset) {
                $this->trigger_error("alloc datasize:" . $dataoffset, E_USER_WARNING);
                return false;
            }
            $this->_puts($dataoffset, $data);
            $this->_set_schema($schema_id, "miss", $this->_get_schema($schema_id, "miss") + 1);
            $this->_lru_push($schema_id, $hdseq);
            $this->unlock();
            return true;
        } else {
            $this->trigger_error("Couldn't lock the file !", E_USER_WARNING);
            return false;
        }
    }

    public function search($key, &$pos)
    {
        return $this->_get_pos_by_key($this->_get_node_root($key), $key, $pos);
    }

    public function _get_size_schema_id($size)
    {
        foreach ($this->_block_size_list as $k => $block_size) {
            if ($size <= $block_size) {
                return $k;
            }
        }
        return false;
    }

    public function _parse_str_size($str_size, $default)
    {
        if (preg_match("/^([0-9]+)\\s*([gmk]|)\$/i", $str_size, $match)) {
            switch (strtolower($match[2])) {
                case "g" :
                    if (1 < $match[1]) {
                        $this->trigger_error("Max cache size 1G", E_USER_ERROR);
                    }
                    $size = $match[1] << 30;
                    break;
                case "m" :
                    $size = $match[1] << 20;
                    break;
                case "k" :
                    $size = $match[1] << 10;
                    break;
                default :
                    $size = $match[1];
            }
            if ($size <= 0) {
                $this->trigger_error("Error cache size " . $this->max_size, E_USER_ERROR);
                return false;
            } else if ($size < 10485760) {
                return 10485760;
            } else {
                return $size;
            }
        } else {
            return $default;
        }
    }

    public function _format($truncate = false)
    {
        if ($this->lock(true, true)) {
            if ($truncate) {
                $this->_seek(0);
                ftruncate($this->_rs, $this->idx_node_base);
            }
            $this->max_size = $this->_parse_str_size(SECACHE_SIZE, 15728640);
            $this->_puts($this->header_padding, pack("V1a*", $this->max_size, $this->ver));
            ksort($this->_bsize_list);
            $ds_offset = $this->data_base_pos;
            $i = 0;
            foreach ($this->_bsize_list as $size => $count) {
                $count *= min(3, floor($this->max_size / 10485760));
                $next_free_node = 0;
                $j = 0;
                for (; $j < $count; ++$j) {
                    $this->_puts($ds_offset, pack("V", $next_free_node));
                    $next_free_node = $ds_offset;
                    $ds_offset += intval($size);
                }
                $code = pack(str_repeat("V1", count($this->schema_struct)), $size, $next_free_node, 0, 0, 0, 0);
                $this->_puts(60 + $i * $this->schema_item_size, $code);
                ++$i;
            }
            $this->_set_dcur_pos($ds_offset);
            $this->_puts($this->idx_base_pos, str_repeat("\x00", 262144));
            $this->_puts($this->idx_seq_pos, pack("V", 1));
            $this->unlock();
            return true;
        } else {
            $this->trigger_error("Couldn't lock the file !", E_USER_ERROR);
            return false;
        }
    }

    public function _get_node_root($key)
    {
        $this->_seek(hexdec(substr($key, 0, 4)) * 4 + $this->idx_base_pos);
        $a = fread($this->_rs, 4);
        list(, $offset) = unpack("V", $a);
        return $offset;
    }

    public function _set_node_root($key, $value)
    {
        return $this->_puts(hexdec(substr($key, 0, 4)) * 4 + $this->idx_base_pos, pack("V", $value));
    }

    public function _set_node($pos, $key, $value)
    {
        if (!$pos) {
            return false;
        }
        if (isset($this->_node_struct[$key])) {
            return $this->_puts($pos * $this->idx_node_size + $this->idx_node_base + $this->_node_struct[$key][0], pack($this->_node_struct[$key][1], $value));
        } else {
            return false;
        }
    }

    public function _get_pos_by_key($offset, $key, &$pos)
    {
        if (!$offset) {
            $pos = 0;
            return false;
        }
        $info = $this->_get_node($offset);
        if ($info['key'] == $key) {
            $pos = $info['offset'];
            return true;
        } else if ($info['next'] && $info['next'] != $offset) {
            return $this->_get_pos_by_key($info['next'], $key, $pos);
        } else {
            $pos = $offset;
            return false;
        }
    }

    public function _lru_delete($info)
    {
        if ($info['lru_right']) {
            $this->_set_node($info['lru_right'], "lru_left", $info['lru_left']);
        } else {
            $this->_set_schema($this->_get_size_schema_id($info['size']), "lru_tail", $info['lru_left']);
        }
        if ($info['lru_left']) {
            $this->_set_node($info['lru_left'], "lru_right", $info['lru_right']);
        } else {
            $this->_set_schema($this->_get_size_schema_id($info['size']), "lru_head", $info['lru_right']);
        }
        return true;
    }

    public function _lru_push($schema_id, $offset)
    {
        $lru_head = $this->_get_schema($schema_id, "lru_head");
        $lru_tail = $this->_get_schema($schema_id, "lru_tail");
        if (!$offset || $lru_head == $offset) {
            return;
        }
        $info = $this->_get_node($offset);
        $this->_set_node($info['lru_right'], "lru_left", $info['lru_left']);
        $this->_set_node($info['lru_left'], "lru_right", $info['lru_right']);
        $this->_set_node($offset, "lru_right", $lru_head);
        $this->_set_node($offset, "lru_left", 0);
        $this->_set_node($lru_head, "lru_left", $offset);
        $this->_set_schema($schema_id, "lru_head", $offset);
        if ($lru_tail == 0) {
            $this->_set_schema($schema_id, "lru_tail", $offset);
        } else if ($lru_tail == $offset && $info['lru_left']) {
            $this->_set_schema($schema_id, "lru_tail", $info['lru_left']);
        }
        return true;
    }

    public function _get_node($offset)
    {
        $this->_seek($offset * $this->idx_node_size + $this->idx_node_base);
        $info = unpack("V1next/V1prev/V1data/V1size/V1lru_right/V1lru_left/H*key", fread($this->_rs, $this->idx_node_size));
        $info['offset'] = $offset;
        return $info;
    }

    public function _lru_pop($schema_id)
    {
        if ($node = $this->_get_schema($schema_id, "lru_tail")) {
            $info = $this->_get_node($node);
            if (!$info['data']) {
                return false;
            }
            $this->delete($info['key'], $info['offset']);
            if (!$this->_get_schema($schema_id, "free")) {
                $this->trigger_error("pop lru,But nothing free...", E_USER_ERROR);
            }
            return $info;
        } else {
            return false;
        }
    }

    public function _dalloc($schema_id, $lru_freed = false)
    {
        if ($free = $this->_get_schema($schema_id, "free")) {
            $this->_seek($free);
            list(, $next) = unpack("V", fread($this->_rs, 4));
            $this->_set_schema($schema_id, "free", $next);
            return $free;
        } else if ($lru_freed) {
            $this->trigger_error("Bat lru poped freesize", E_USER_ERROR);
            return false;
        } else {
            $ds_offset = $this->_get_dcur_pos();
            $size = $this->_get_schema($schema_id, "size");
            if ($this->max_size < $size + $ds_offset) {
                if ($info = $this->_lru_pop($schema_id)) {
                    return $this->_dalloc($schema_id, $info);
                } else {
                    $this->trigger_error("Can't alloc dataspace", E_USER_ERROR);
                    return false;
                }
            } else {
                $this->_set_dcur_pos($ds_offset + $size);
                return $ds_offset;
            }
        }
    }

    public function _get_dcur_pos()
    {
        $this->_seek($this->dfile_cur_pos);
        list(, $ds_offset) = unpack("V", fread($this->_rs, 4));
        return $ds_offset;
    }

    public function _set_dcur_pos($pos)
    {
        return $this->_puts($this->dfile_cur_pos, pack("V", $pos));
    }

    public function _free_dspace($size, $pos)
    {
        if ($this->max_size < $pos) {
            $this->trigger_error("free dspace over quota:" . $pos, E_USER_ERROR);
            return false;
        }
        $schema_id = $this->_get_size_schema_id($size);
        if ($free = $this->_get_schema($schema_id, "free")) {
            $this->_puts($free, pack("V1", $pos));
        } else {
            $this->_set_schema($schema_id, "free", $pos);
        }
        $this->_puts($pos, pack("V1", 0));
    }

    public function _dfollow($pos, &$c)
    {
        ++$c;
        $this->_seek($pos);
        list(, $next) = unpack("V1", fread($this->_rs, 4));
        if ($next) {
            return $this->_dfollow($next, $c);
        } else {
            return $pos;
        }
    }

    public function _free_node($pos)
    {
        $this->_seek($this->idx_free_pos);
        list(, $prev_free_node) = unpack("V", fread($this->_rs, 4));
        $this->_puts($pos * $this->idx_node_size + $this->idx_node_base, pack("V", $prev_free_node) . str_repeat("\x00", $this->idx_node_size - 4));
        return $this->_puts($this->idx_free_pos, pack("V", $pos));
    }

    public function _alloc_idx($data)
    {
        $this->_seek($this->idx_free_pos);
        list(, $list_pos) = unpack("V", fread($this->_rs, 4));
        if ($list_pos) {
            $this->_seek($list_pos * $this->idx_node_size + $this->idx_node_base);
            list(, $prev_free_node) = unpack("V", fread($this->_rs, 4));
            $this->_puts($this->idx_free_pos, pack("V", $prev_free_node));
        } else {
            $this->_seek($this->idx_seq_pos);
            list(, $list_pos) = unpack("V", fread($this->_rs, 4));
            $this->_puts($this->idx_seq_pos, pack("V", $list_pos + 1));
        }
        return $this->_create_node($list_pos, $data);
    }

    public function _create_node($pos, $data)
    {
        $this->_puts($pos * $this->idx_node_size + $this->idx_node_base, pack("V1V1V1V1V1V1H*", $data['next'], $data['prev'], $data['data'], $data['size'], $data['lru_right'], $data['lru_left'], $data['key']));
        return $pos;
    }

    public function _set_schema($schema_id, $key, $value)
    {
        $info = array_flip($this->schema_struct);
        return $this->_puts(60 + $schema_id * $this->schema_item_size + $info[$key] * 4, pack("V", $value));
    }

    public function _get_schema($id, $key)
    {
        $info = array_flip($this->schema_struct);
        $this->_seek(60 + $id * $this->schema_item_size);
        unpack("V1" . implode("/V1", $this->schema_struct), fread($this->_rs, $this->schema_item_size));
        $this->_seek(60 + $id * $this->schema_item_size + $info[$key] * 4);
        list(, $value) = unpack("V", fread($this->_rs, 4));
        return $value;
    }

    public function _all_schemas()
    {
        $schema = array();
        $i = 0;
        for (; $i < 16; ++$i) {
            $this->_seek(60 + $i * $this->schema_item_size);
            $info = unpack("V1" . implode("/V1", $this->schema_struct), fread($this->_rs, $this->schema_item_size));
            if ($info['size']) {
                $info['id'] = $i;
                $schema[$i] = $info;
            } else {
                return $schema;
            }
        }
    }

    public function schemaStatus()
    {
        $return = array();
        foreach ($this->_all_schemas() as $k => $schemaItem) {
            if ($schemaItem['free']) {
                $this->_dfollow($schemaItem['free'], $schemaItem['freecount']);
            }
            $return[] = $schemaItem;
        }
        return $return;
    }

    public function status(&$curBytes, &$totalBytes)
    {
        $totalBytes = $curBytes = 0;
        $hits = $miss = 0;
        $schemaStatus = $this->schemaStatus();
        $totalBytes = $this->max_size;
        $freeBytes = $this->max_size - $this->_get_dcur_pos();
        foreach ($schemaStatus as $schema) {
            $freeBytes += $schema['freecount'] * $schema['size'];
            $miss += $schema['miss'];
            $hits += $schema['hits'];
        }
        $curBytes = $totalBytes - $freeBytes;
        $return[] = array(
            "name" => __("缓存命中"),
            "value" => $hits
        );
        $return[] = array(
            "name" => __("缓存未命中"),
            "value" => $miss
        );
        return $return;
    }

    public function trigger_error($errstr, $errno)
    {
        if ($errno == E_USER_ERROR) {
            if (!$this->_in_fatal_error) {
                $this->_in_fatal_error = true;
                $this->_format(true);
            }
            header("HTTP/1.1 500 Internal Server Error", true, 500);
            if (function_exists("debug_print_backtrace")) {
                echo "<h1>" . $errstr . "</h1><hr />";
                echo "<pre>";
                debug_print_backtrace();
                echo "</pre>";
                exit();
            } else {
                trigger_error($errstr, $errno);
            }
        } else {
            trigger_error($errstr, $errno);
        }
    }

}

?>
